/*
 * Hibernate Search, full-text search for your domain model
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.search.integrationtest.mapper.orm.bootstrap;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.Entity;
import javax.persistence.Id;

import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.boot.spi.BootstrapContext;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.search.engine.cfg.BackendSettings;
import org.hibernate.search.engine.cfg.EngineSettings;
import org.hibernate.search.mapper.orm.bootstrap.spi.HibernateOrmIntegrationBooter;
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
import org.hibernate.search.mapper.orm.mapping.HibernateOrmMappingConfigurationContext;
import org.hibernate.search.mapper.orm.mapping.HibernateOrmSearchMappingConfigurer;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;
import org.hibernate.search.util.common.impl.Closer;
import org.hibernate.search.util.impl.integrationtest.common.rule.BackendMock;
import org.hibernate.search.util.impl.integrationtest.common.stub.backend.index.StubSchemaManagementWork;
import org.hibernate.search.util.impl.integrationtest.mapper.orm.OrmUtils;
import org.hibernate.search.util.impl.integrationtest.mapper.orm.SimpleSessionFactoryBuilder;

import org.junit.After;
import org.junit.Rule;
import org.junit.Test;

import org.assertj.core.api.Fail;

public class HibernateOrmIntegrationBooterIT {

	private static final String INDEX_NAME = "IndexName";

	private final List<AutoCloseable> toClose = new ArrayList<>();

	@Rule
	public BackendMock backendMock = new BackendMock();

	@After
	public void cleanup() throws Exception {
		try ( Closer<Exception> closer = new Closer<>() ) {
			closer.pushAll( AutoCloseable::close, toClose );
		}
	}

	@Test
	public void twoPhaseBoot() {
		HibernateOrmIntegrationBooter booter = createBooter( IndexedEntity.class );
		Map<String, Object> booterGeneratedProperties = new LinkedHashMap<>();

		// Pre-booting should lead to a schema definition in the backend.
		backendMock.expectSchema( INDEX_NAME, b -> { } );
		booter.preBoot( booterGeneratedProperties::put );
		backendMock.verifyExpectationsMet();

		SimpleSessionFactoryBuilder builder = new SimpleSessionFactoryBuilder()
				.addAnnotatedClass( IndexedEntity.class )
				/*
				 * We use a "trapped" mapping configurer to check that Hibernate Search does not generate the mapping,
				 * but re-uses the one generated by the "pre-boot" above.
				 */
				.setProperty( HibernateOrmMapperSettings.MAPPING_CONFIGURER, new HibernateOrmSearchMappingConfigurer() {
					@Override
					public void configure(HibernateOrmMappingConfigurationContext context) {
						Fail.fail( "Hibernate Search did not re-use the mapping generated when pre-booting" );
					}
				} );

		for ( Map.Entry<String, Object> booterGeneratedProperty : booterGeneratedProperties.entrySet() ) {
			builder.setProperty( booterGeneratedProperty.getKey(), booterGeneratedProperty.getValue() );
		}

		// Actually booting the session factory should lead to a schema creation in the backend.
		backendMock.expectSchemaManagementWorks( INDEX_NAME )
				.work( StubSchemaManagementWork.Type.CREATE_OR_VALIDATE );
		try ( SessionFactory sessionFactory = builder.build() ) {
			/*
			 * Building the session should NOT lead to a second schema creation in the backend:
			 * that would mean the pre-boot was ignored...
			 */
			backendMock.verifyExpectationsMet();

			OrmUtils.withinTransaction( sessionFactory, session -> {
				IndexedEntity entity = new IndexedEntity();
				entity.id = 1;
				session.persist( entity );

				backendMock.expectWorks( INDEX_NAME )
						.add( "1", b -> { } )
						.processedThenExecuted();
			} );
			// If the entity was indexed, it means Hibernate Search booted correctly
			backendMock.verifyExpectationsMet();
		}
	}

	private HibernateOrmIntegrationBooter createBooter(Class<?> ... entityClasses) {
		StandardServiceRegistryBuilder registryBuilder = new StandardServiceRegistryBuilder();

		// Configure the backend
		registryBuilder.applySetting(
				EngineSettings.BACKEND + "." + BackendSettings.TYPE,
				backendMock.factory()
		);

		StandardServiceRegistry serviceRegistry = registryBuilder.build();
		toClose.add( serviceRegistry );

		MetadataSources metadataSources = new MetadataSources( serviceRegistry );
		for ( Class<?> entityClass : entityClasses ) {
			metadataSources.addAnnotatedClass( entityClass );
		}
		Metadata metadata = metadataSources.buildMetadata();

		MetadataImplementor metadataImplementor = (MetadataImplementor) metadata;
		BootstrapContext bootstrapContext =
				metadataImplementor.getTypeConfiguration().getMetadataBuildingContext().getBootstrapContext();

		return HibernateOrmIntegrationBooter.create(
				metadata, bootstrapContext
		);
	}

	@Entity
	@Indexed(index = INDEX_NAME)
	private static class IndexedEntity {
		@Id
		private Integer id;
	}
}
